{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "32652b67",
   "metadata": {},
   "source": [
    "# RunnableParallel: 데이터 병렬 조작\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7d22acf",
   "metadata": {},
   "source": [
    "## 입력 및 출력 조작\n",
    "\n",
    "`RunnableParallel` 은 시퀀스 내에서 하나의 `Runnable` 의 출력을 다음 `Runnable` 의 입력 형식에 맞게 조작하는 데 유용하게 사용될 수 있습니다.\n",
    "\n",
    "여기서 prompt에 대한 입력은 \"context\"와 \"question\"이라는 키를 가진 map 형태로 예상됩니다.\n",
    "\n",
    "사용자 입력은 단순히 질문 내용입니다. 따라서 retriever를 사용하여 컨텍스트를 가져오고, 사용자 입력을 \"question\" 키 아래에 전달해야 합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ed88e4d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81de3c1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH13-LCEL-Advanced\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b7c0e40",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
    "\n",
    "# 텍스트로부터 FAISS 벡터 저장소를 생성합니다.\n",
    "vectorstore = FAISS.from_texts(\n",
    "    [\"Teddy is an AI engineer who loves programming!\"], embedding=OpenAIEmbeddings()\n",
    ")\n",
    "# 벡터 저장소를 검색기로 사용합니다.\n",
    "retriever = vectorstore.as_retriever()\n",
    "# 템플릿을 정의합니다.\n",
    "template = \"\"\"Answer the question based only on the following context:\n",
    "{context}\n",
    "\n",
    "Question: {question}\n",
    "\"\"\"\n",
    "# 템플릿으로부터 채팅 프롬프트를 생성합니다.\n",
    "prompt = ChatPromptTemplate.from_template(template)\n",
    "\n",
    "# ChatOpenAI 모델을 초기화합니다.\n",
    "model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "\n",
    "# 검색 체인을 구성합니다.\n",
    "retrieval_chain = (\n",
    "    {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 검색 체인을 실행하여 질문에 대한 답변을 얻습니다.\n",
    "retrieval_chain.invoke(\"What is Teddy's occupation?\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3579513c",
   "metadata": {},
   "source": [
    "다른 `Runnable` 과 함께 `RunnableParallel` 을 구성할 때, 유형 변환이 자동으로 처리되므로 `RunnableParallel` 클래스에서 입력으로 주입되는 dict 입력을 별도 래핑할 필요도 없다는 점에 유의하세요.\n",
    "\n",
    "아래의 3가지 방식은 모두 동일하게 처리합니다.\n",
    "\n",
    "```python\n",
    "# 자체 RunnableParallel 로 래핑됨\n",
    "1. {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    "\n",
    "2. RunnableParallel({\"context\": retriever, \"question\": RunnablePassthrough()})\n",
    "\n",
    "3. RunnableParallel(context=retriever, question=RunnablePassthrough())\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd4d7245",
   "metadata": {},
   "source": [
    "## itemgetter를 단축어로 사용하기\n",
    "\n",
    "RunnableParallel과 결합할 때 Python의 `itemgetter`를 단축어로 사용하여 map에서 데이터를 추출할 수 있습니다.\n",
    "\n",
    "- [참고] itemgetter에 대한 자세한 정보는 [Python Documentation](https://docs.python.org/3/library/operator.html#operator.itemgetter)에서 확인할 수 있습니다.\n",
    "\n",
    "아래 예제에서는 `itemgetter` 를 사용하여 map에서 특정 키를 추출합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1406d0d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
    "\n",
    "# 텍스트로부터 FAISS 벡터 저장소를 생성합니다.\n",
    "vectorstore = FAISS.from_texts(\n",
    "    [\"Teddy is an AI engineer who loves programming!\"], embedding=OpenAIEmbeddings()\n",
    ")\n",
    "# 벡터 저장소를 검색기로 사용합니다.\n",
    "retriever = vectorstore.as_retriever()\n",
    "\n",
    "# 템플릿을 정의합니다.\n",
    "template = \"\"\"Answer the question based only on the following context:\n",
    "{context}\n",
    "\n",
    "Question: {question}\n",
    "\n",
    "Answer in the following language: {language}\n",
    "\"\"\"\n",
    "# 템플릿으로부터 채팅 프롬프트를 생성합니다.\n",
    "prompt = ChatPromptTemplate.from_template(template)\n",
    "\n",
    "# 체인을 구성합니다.\n",
    "chain = (\n",
    "    {\n",
    "        \"context\": itemgetter(\"question\") | retriever,\n",
    "        \"question\": itemgetter(\"question\"),\n",
    "        \"language\": itemgetter(\"language\"),\n",
    "    }\n",
    "    | prompt\n",
    "    | ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 체인을 호출하여 질문에 답변합니다.\n",
    "chain.invoke({\"question\": \"What is Teddy's occupation?\", \"language\": \"Korean\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ee28026",
   "metadata": {},
   "source": [
    "## 병렬처리를 단계별로 이해\n",
    "\n",
    "`RunnableParallel` 을 사용하면 여러 `Runnable` 을 병렬로 실행하고, 이러한 `Runnable` 의 출력을 맵(map)으로 반환하는 것이 쉬워집니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14b85a96",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnableParallel\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI()  # ChatOpenAI 모델을 초기화합니다.\n",
    "\n",
    "# 수도를 묻는 질문에 대한 체인을 정의합니다.\n",
    "capital_chain = (\n",
    "    ChatPromptTemplate.from_template(\"{country} 의 수도는 어디입니까?\")\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 면적을 묻는 질문에 대한 체인을 정의합니다.\n",
    "area_chain = (\n",
    "    ChatPromptTemplate.from_template(\"{country} 의 면적은 얼마입니까?\")\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# capital_chain, area_chain 을 병렬로 실행할 수 있는 RunnableParallel 객체를 생성합니다.\n",
    "map_chain = RunnableParallel(capital=capital_chain, area=area_chain)\n",
    "\n",
    "# map_chain을 호출하여 대한민국의 수도와 면적을 묻습니다.\n",
    "map_chain.invoke({\"country\": \"대한민국\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e7176bc",
   "metadata": {},
   "source": [
    "아래와 같이 chain 별로 입력 템플릿의 변수가 달라도 상관없이 실행 가능합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9a0a89b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 수도를 묻는 질문에 대한 체인을 정의합니다.\n",
    "capital_chain2 = (\n",
    "    ChatPromptTemplate.from_template(\"{country1} 의 수도는 어디입니까?\")\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 면적을 묻는 질문에 대한 체인을 정의합니다.\n",
    "area_chain2 = (\n",
    "    ChatPromptTemplate.from_template(\"{country2} 의 면적은 얼마입니까?\")\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# capital_chain, area_chain 을 병렬로 실행할 수 있는 RunnableParallel 객체를 생성합니다.\n",
    "map_chain2 = RunnableParallel(capital=capital_chain2, area=area_chain2)\n",
    "\n",
    "# map_chain을 호출합니다. 이때 각각의 key에 대한 value를 전달합니다.\n",
    "map_chain2.invoke({\"country1\": \"대한민국\", \"country2\": \"미국\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "231296a1",
   "metadata": {},
   "source": [
    "## 병렬 처리\n",
    "\n",
    "`RunnableParallel` 은 맵에 있는 각 `Runnable` 이 병렬로 실행되기 때문에 독립적인 프로세스를 병렬로 실행하는 데에도 유용합니다.\n",
    "\n",
    "예를 들어, 앞서 살펴본 `area_chain`, `capital_chain`, `map_chain`은 `map_chain`이 다른 두 체인을 모두 실행함에도 불구하고 **거의 동일한 실행 시간** 을 가지는 것을 확인할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b55da4f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%timeit\n",
    "\n",
    "# 면저을 묻는 체인을 호출하고 실행 시간을 측정합니다.\n",
    "area_chain.invoke({\"country\": \"대한민국\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d12617c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%timeit\n",
    "\n",
    "# 수도를 묻는 체인을 호출하고 실행 시간을 측정합니다.\n",
    "capital_chain.invoke({\"country\": \"대한민국\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5efe2c40",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%timeit\n",
    "\n",
    "# Parallel 하게 구성된 체인을 호출하고 실행 시간을 측정합니다.\n",
    "map_chain.invoke({\"country\": \"대한민국\"})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
